import * as THREE from 'three'

export class FirstPersonCameraControl {
  constructor (camera, domElement, rayCastObjects) {
    this.camera = camera
    this.domElement = domElement // dom
    this._isEnabled = true // 是否启用
    // internal params for move forward/right
    this._rayCastObjects = rayCastObjects // 射线可投射对象
    this._rayOriginOffset = new THREE.Vector3(0, -1, 0) // 射线起点
    this._camerLocalDirection = new THREE.Vector3() //
    this._tmpVector = new THREE.Vector3() // 归一化临时方向向量，用来记录射线xyz三个方向
    this._tmpFar = 10 // 射线最远距离
    this._rayCaster = new THREE.Raycaster(null, null, 0, this._tmpFar) // 初始化射线
    this._fallingTime = 0
    // internal params for mouse move rotation
    this._euler = new THREE.Euler(0, 0, 0, 'YZX')
    this._prevMouseX = 0
    this._prevMouseY = 0
    // public settings
    this.applyGravity = true // 应用重力
    this.applyCollision = true // 应用碰撞
    this.positionEasing = true // 应用位置宽松
    this.lookflag = 1
    this.lookSpeed = 0.008 // 视角移动速度
    this.moveSpeed = 1 // 移速
    this.playerHeight = 10 // 距地面高度
    this.collisionDistance = 5 // 据墙壁距离
    this.g = 9.8 // 重力系数
    // event bindings
    this.bindonKeyDown = this.onKeyDown.bind(this)
    this.bindonKeyUp = this.onKeyUp.bind(this)
    // jump
    this.applyJump = false
    this.prevTime = performance.now()
    this.jumpY = 0

    // init
    this._euler.setFromQuaternion(this.camera.quaternion)
    this.addEvents()
  }

  /**
   * @param  {Object} colliders set objects for collision detection
   */
  set colliders (colliders) {
    this._rayCastObjects = colliders
  }

  /**
   * @param {boolean} isEnabled set if this camera control is enabled
   */
  set enabled (isEnabled) {
    if (this._isEnabled != isEnabled) {
      this._isEnabled = isEnabled
      this._euler.setFromQuaternion(this.camera.quaternion)
      if (isEnabled) {
        this.addEvents()
      } else {
        this.removeEvents()
      }
    }
  }

  /**
   * @description: getter if current camera control is enabled.
   */
  get enabled () {
    return this._isEnabled
  }

  addEvents () {
    document.body.addEventListener('keydown', this.bindonKeyDown, false)
    document.body.addEventListener('keyup', this.bindonKeyUp, false)
  }

  removeEvents () {
    document.body.removeEventListener('keydown', this.bindonKeyDown)
    document.body.removeEventListener('keyup', this.bindonKeyUp)
  }

  // 键盘按下 监听
  onKeyDown (event) {
    switch (event.keyCode) {
      case 38: // up
        this.rotateY(-1)
        break
      case 87: // w
        this._camerLocalDirection.z = 1
        break

      case 37: // left
        this.rotateX(-1)
        break
      case 65: // a
        this._camerLocalDirection.x = -1
        break

      case 40: // down
        this.rotateY(1)
        break
      case 83: // s
        this._camerLocalDirection.z = -1
        break

      case 39: // right
        this.rotateX(1)
        break
      case 68: // d
        this._camerLocalDirection.x = 1
        break

      case 32: // space
        if (this.canJump === true) this.jumpY = 200
        this.canJump = false
        break
    }
  }

  onKeyUp (event) {
    switch (event.keyCode) {
      case 38: // up
      case 87: // w
        this._camerLocalDirection.z = 0
        break

      case 37: // left
      case 65: // a
        this._camerLocalDirection.x = 0
        break

      case 40: // down
      case 83: // s
        this._camerLocalDirection.z = 0
        break

      case 39: // right
      case 68: // d
        this._camerLocalDirection.x = 0
        break
    }
  }

  /**
   * @description: rotate camera by left/right
   * @param {Number} value
   * @return: null
   */
  rotateX (value) {
    this._euler.y -= value * this.lookSpeed
    this.camera.quaternion.setFromEuler(this._euler)
  }

  /**
   * @description: rotate camera by up/down
   * @param  {Number} value
   * @return: null
   */
  rotateY (value) {
    this._euler.x -= value * this.lookflag * 0.5 * this.lookSpeed
    this.camera.quaternion.setFromEuler(this._euler)
  }

  /**
   * @description: update current calcuate each frame.
   */
  update () {
    //gravity test
    this.gravityTest()
    //collision test
    this.collisionTest()
    //jump test
    this.jumpTest()
  }

  gravityTest () {
    if (this.applyGravity && this._rayCastObjects) {
      let isFalling = true
      this._fallingTime += 0.01
      this._tmpVector.set(0, -1, 0)
      this._tmpFar = this.playerHeight
      const intersect = this.hitTest()
      if (intersect) {
        const newPosition = intersect.point.add(
          new THREE.Vector3(0, this.playerHeight, 0)
        )
        if (this.positionEasing) {
          if (
            newPosition.y >= this.camera.position.y ||
            newPosition.y - this.camera.position.y < 0.2
          ) {
            //上下楼梯时逐步上升 以免明显顿挫感

            this.camera.position.y +=
              (newPosition.y - this.camera.position.y) * 0.08
            this._fallingTime = 0
            isFalling = false
            return
          }
        } else if (intersect.distance < this.playerHeight) {
          this.camera.position.y = newPosition.y
          this._fallingTime = 0
          isFalling = false
        }
      }

      if (isFalling) {
        this.camera.position.y -=
          this.g * Math.pow(this._fallingTime, 2)
      }
    }
  }

  collisionTest () {
    if (this._camerLocalDirection.x !== 0) this.collisionTestX()
    if (this._camerLocalDirection.z !== 0) this.collisionTestZ()
  }

  collisionTestX () {
    this._tmpVector.setFromMatrixColumn(this.camera.matrix, 0)
    this._tmpVector.multiplyScalar(this._camerLocalDirection.x)
    this._tmpFar = this.collisionDistance
    if (this.applyCollision) {
      const intersect = this.hitTest()
      if (intersect && intersect.distance < this.collisionDistance) {
        return
      }
    }

    this.camera.position.addScaledVector(this._tmpVector, this.moveSpeed)
  }

  collisionTestZ () {
    this._tmpVector.setFromMatrixColumn(this.camera.matrix, 0)
    this._tmpVector.crossVectors(this.camera.up, this._tmpVector)
    this._tmpVector.multiplyScalar(this._camerLocalDirection.z)
    this._tmpFar = this.collisionDistance
    if (this.applyCollision) {
      const intersect = this.hitTest()
      if (intersect && intersect.distance < this.collisionDistance) {
        return
      }
    }

    this.camera.position.addScaledVector(this._tmpVector, this.moveSpeed)
  }

  jumpTest () {
    if (this._rayCastObjects && this.applyJump) {
      this._tmpVector.set(0, -1, 0)
      this._tmpFar = this.playerHeight
      var time = performance.now()
      var delta = (time - this.prevTime) / 1000
      this.jumpY -= this.g * 100.0 * delta
      const intersect = this.hitTest()
      if (intersect) {
        this.jumpY = Math.max(0, this.jumpY)
        if (this.jumpY <= 0) this.canJump = true
      }

      if (this.jumpY > 0) this.camera.position.y += (this.jumpY * delta) // new behavior

      if (intersect) {
        if (intersect.distance <= this.playerHeight) {
          this.jumpY = 0
          this.canJump = true
        }
      }
      this.prevTime = time
    }
  }

  hitTest () {
    let result = null
    const origin = this.camera.position.clone().add(this._rayOriginOffset)
    this._rayCaster.ray.origin = origin
    this._rayCaster.ray.direction = this._tmpVector
    const intersect = this._rayCaster.intersectObject(
      this._rayCastObjects,
      true
    )
    if (intersect && intersect.length > 0) {
      result = intersect[0]
    }
    return result;
  }
}
